/*
 * Sylvain BERTRAND <digital.ragnarok@gmail.com>
 * code protected by GNU GPL v3
 */
#include <X11/keysym.h>
#include <X11/Xutil.h>
#include <glib.h>
#include "display.h"
#include "keyboard.h"
#define NUM_MASKS 8
#define ALL_MASKS 0xff
#define nth_mask(n) (1 << n)
static void xim_init(void);
void t_keyboard_shutdown();
void t_keyboard_context_renew(struct t_ic *ic);
static XModifierKeymap *modmap;
static KeySym *keymap;
static gint min_keycode, max_keycode, keysyms_per_keycode;
static guchar modkeys_keys[T_KEYBOARD_NUM_MODKEYS];
static gboolean alt_l = FALSE;
static gboolean meta_l = FALSE;
static gboolean super_l = FALSE;
static gboolean hyper_l = FALSE;
static gboolean started = FALSE;
static XIM xim = NULL;
static XIMStyle xim_style = 0;
static GSList *xic_all = NULL;

static void set_modkey_mask(guchar mask, KeySym sym)
{
	if(sym == XK_Num_Lock)
		modkeys_keys[T_KEYBOARD_MODKEY_NUMLOCK] |= mask;
	else if(sym == XK_Scroll_Lock)
		modkeys_keys[T_KEYBOARD_MODKEY_SCROLLLOCK] |= mask;
	else if(sym == XK_Super_L && super_l)
		modkeys_keys[T_KEYBOARD_MODKEY_SUPER] |= mask;
	else if(sym == XK_Super_L && !super_l)
		modkeys_keys[T_KEYBOARD_MODKEY_SUPER] = mask, super_l = TRUE;
	else if(sym == XK_Super_R && !super_l)
		modkeys_keys[T_KEYBOARD_MODKEY_SUPER] |= mask;
	else if(sym == XK_Hyper_L && hyper_l)
		modkeys_keys[T_KEYBOARD_MODKEY_HYPER] |= mask;
	else if(sym == XK_Hyper_L && !hyper_l)
		modkeys_keys[T_KEYBOARD_MODKEY_HYPER] = mask, hyper_l = TRUE;
	else if(sym == XK_Hyper_R && !hyper_l)
		modkeys_keys[T_KEYBOARD_MODKEY_HYPER] |= mask;
	else if(sym == XK_Alt_L && alt_l)
		modkeys_keys[T_KEYBOARD_MODKEY_ALT] |= mask;
	else if(sym == XK_Alt_L && !alt_l)
		modkeys_keys[T_KEYBOARD_MODKEY_ALT] = mask, alt_l = TRUE;
	else if(sym == XK_Alt_R && !alt_l)
		modkeys_keys[T_KEYBOARD_MODKEY_ALT] |= mask;
	else if(sym == XK_Meta_L && meta_l)
		modkeys_keys[T_KEYBOARD_MODKEY_META] |= mask;
	else if(sym == XK_Meta_L && !meta_l)
		modkeys_keys[T_KEYBOARD_MODKEY_META] = mask, meta_l = TRUE;
	else if(sym == XK_Meta_R && !meta_l)
		modkeys_keys[T_KEYBOARD_MODKEY_META] |= mask;
}

void t_keyboard_reload(void)
{
	gint i, j, k;
	if(started)
		t_keyboard_shutdown();
	started = TRUE;
	xim_init();
	for(i = 0; i < T_KEYBOARD_NUM_MODKEYS; ++i)
		modkeys_keys[i] = 0;
	modmap = XGetModifierMapping(t_display);
	XDisplayKeycodes(t_display, &min_keycode, &max_keycode);
	keymap = XGetKeyboardMapping(t_display, min_keycode, max_keycode - min_keycode + 1, &keysyms_per_keycode);
	alt_l = meta_l = super_l = hyper_l = FALSE;
	for(i = 0; i < NUM_MASKS; ++i) {
		for(j = 0; j < modmap->max_keypermod; ++j) {
			KeySym sym;
			KeyCode keycode = modmap->modifiermap[i * modmap->max_keypermod + j];
			if(keycode) {
				for(k = 0; k < keysyms_per_keycode; ++k) {
					sym = keymap[(keycode - min_keycode) * keysyms_per_keycode + k];
					if(sym != NoSymbol) {
						set_modkey_mask(nth_mask(i), sym);
					}
				}
			}
		}
	}
	modkeys_keys[T_KEYBOARD_MODKEY_CAPSLOCK] = LockMask;
	modkeys_keys[T_KEYBOARD_MODKEY_SHIFT] = ShiftMask;
	modkeys_keys[T_KEYBOARD_MODKEY_CONTROL] = ControlMask;
}

void t_keyboard_shutdown(void)
{
	GSList *it;
	XFreeModifiermap(modmap);
	modmap = NULL;
	XFree(keymap);
	keymap = NULL;
	for(it = xic_all; it; it = g_slist_next(it)) {
		struct t_ic *ic = it->data;
		if(ic->xic) {
			XDestroyIC(ic->xic);
			ic->xic = NULL;
		}
	}
	if(xim)
		XCloseIM(xim);
	xim = NULL;
	xim_style = 0;
	started = FALSE;
}

void xim_init(void)
{
	GSList *it;
	gchar *aname, *aclass;
	aname = g_strdup(g_get_prgname());
	if(!aname)
		aname = g_strdup("obt");
	aclass = g_strdup(aname);
	if(g_ascii_islower(aclass[0]))
		aclass[0] = g_ascii_toupper(aclass[0]);
	xim = XOpenIM(t_display, NULL, aname, aclass);
	if(!xim)
		g_message("Failed to open an Input Method");
	else {
		XIMStyles *xim_styles = NULL;
		char *r;
		r = XGetIMValues(xim, XNQueryInputStyle, &xim_styles, NULL);
		if(r || !xim_styles)
			g_message("Input Method does not support any styles");
		if(xim_styles) {
			int i;
			for(i = 0; i < xim_styles->count_styles; ++i) {
				if(xim_styles->supported_styles[i] == (XIMPreeditNothing | XIMStatusNothing)) {
					xim_style = xim_styles->supported_styles[i];
					break;
				}
			}
			XFree(xim_styles);
		}
		if(!xim_style) {
			g_message("Input Method does not support a usable style");
			XCloseIM(xim);
			xim = NULL;
		}
	}
	for(it = xic_all; it; it = g_slist_next(it))
		t_keyboard_context_renew(it->data);
	g_free(aclass);
	g_free(aname);
}

static enum t_modkeys_key keyevent_to_modkey(XEvent *e)
{
	KeySym sym;
	g_return_val_if_fail(e->type == KeyPress || e->type == KeyRelease, T_KEYBOARD_MODKEY_NONE);
	XLookupString(&e->xkey, NULL, 0, &sym, NULL);
	switch (sym) {
	case XK_Num_Lock:
		return T_KEYBOARD_MODKEY_NUMLOCK;
	case XK_Scroll_Lock:
		return T_KEYBOARD_MODKEY_SCROLLLOCK;
	case XK_Caps_Lock:
		return T_KEYBOARD_MODKEY_SHIFT;
	case XK_Alt_L:
	case XK_Alt_R:
		return T_KEYBOARD_MODKEY_ALT;
	case XK_Super_L:
	case XK_Super_R:
		return T_KEYBOARD_MODKEY_SUPER;
	case XK_Hyper_L:
	case XK_Hyper_R:
		return T_KEYBOARD_MODKEY_HYPER;
	case XK_Meta_L:
	case XK_Meta_R:
		return T_KEYBOARD_MODKEY_META;
	case XK_Control_L:
	case XK_Control_R:
		return T_KEYBOARD_MODKEY_CONTROL;
	case XK_Shift_L:
	case XK_Shift_R:
		return T_KEYBOARD_MODKEY_SHIFT;
	default:
		return T_KEYBOARD_MODKEY_NONE;
	}
}

guint t_keyboard_keyevent_to_modmask(XEvent *e)
{
	g_return_val_if_fail(e->type == KeyPress || e->type == KeyRelease, 0);
	return t_keyboard_modkey_to_modmask(keyevent_to_modkey(e));
}

guint t_keyboard_only_modmasks(guint mask)
{
	mask &= ALL_MASKS;
	mask &= ~LockMask;
	mask &= ~t_keyboard_modkey_to_modmask(T_KEYBOARD_MODKEY_NUMLOCK);
	mask &= ~t_keyboard_modkey_to_modmask(T_KEYBOARD_MODKEY_SCROLLLOCK);
	return mask;
}

guint t_keyboard_modkey_to_modmask(enum t_modkeys_key key)
{
	return modkeys_keys[key];
}

KeySym t_keyboard_keypress_to_keysym(XEvent *ev)
{
	KeySym sym;
	gint r;
	g_return_val_if_fail(ev->type == KeyPress, None);
	sym = None;
	r = XLookupString(&ev->xkey, NULL, 0, &sym, NULL);
	return sym;
}

void t_keyboard_context_renew(struct t_ic *ic)
{
	if(xim) {
		ic->xic = XCreateIC(xim, XNInputStyle, xim_style, XNClientWindow, ic->client, XNFocusWindow, ic->focus, NULL);
		if(!ic->xic)
			g_message("Error creating Input Context for window 0x%x 0x%x\n", (guint)ic->client, (guint)ic->focus);
	}
}

struct t_ic *t_keyboard_context_new(Window client, Window focus)
{
	struct t_ic *ic;
	g_return_val_if_fail(client != None && focus != None, NULL);
	ic = g_slice_new(struct t_ic);
	ic->ref = 1;
	ic->client = client;
	ic->focus = focus;
	ic->xic = NULL;
	t_keyboard_context_renew(ic);
	xic_all = g_slist_prepend(xic_all, ic);
	return ic;
}

void t_keyboard_context_unref(struct t_ic *ic)
{
	if(--ic->ref < 1) {
		xic_all = g_slist_remove(xic_all, ic);
		if(ic->xic)
			XDestroyIC(ic->xic);
		g_slice_free(struct t_ic, ic);
	}
}
